using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using FirefoxInterface.Properties;
using System.Diagnostics;

namespace FirefoxInterface
{
	/// <summary>
	/// This library gives you the ability to access the profiles set up for Firefox, and to 
	/// view and change the preferences set in the prefs.js file.
	/// 
	/// There is a Profiles collection as a static property of the Profile class which gives you access to
	/// all the profiles and a Default static property which exposes the first profile listed, which is usually the 
	/// only one you need.
	/// 
	/// To view the default profile's preferences access FirefoxInterface.Default.Preferences.GetValue(...
	/// </summary>
	public class Profile
	{
		#region Constants

		private const string PROFILES_PATH = @"\Mozilla\Firefox\";
		private const string PROFILES_FILENAME = "profiles.ini";

		private const string PROFILES_PATTERN =
			@"\[Profile(?<number>[0-9]+)\].+?Name=(?<name>\S+).+?IsRelative=(?<isrelative>\S+).+?Path=(?<path>\S+)";

		#endregion

		#region Constructors

		public Profile(int number, string name, bool isRelative, string path)
		{
			if (Settings.Default.ExceptionOnRunning && InternalHelper.FirefoxRunning)
			{
				throw new Exception("Firefox is currently running, please exit the application before using this.");
			}
			else
			{
				Number = number;
				Name = name;
				IsRelative = isRelative;
				Path = path;
			}
		}

		#endregion

		#region Private Fields

		private static List<Profile> _profiles;

		private int _number;
		private string _name;
		private bool _isRelative;
		private string _path;

		private Preferences _preferences;

		#endregion

		#region Public Instance Properties

		/// <summary>
		/// The absolute path to this profile's folder
		/// </summary>
		public string QualifiedPath
		{
			get
			{
				string path;
				if (IsRelative)
				{
					path = ProfilesPath + Path;
				}
				else
				{
					path = Path;
				}

				return System.IO.Path.GetFullPath(path) + "\\";
			}
		}

		/// <summary>
		/// The path contained in the profiles.ini file for this profile
		/// </summary>
		public string Path
		{
			get { return _path; }
			set { _path = value; }
		}

		/// <summary>
		/// Whether the <see cref="Path"/> is relative to the profiles folder or an absolute path
		/// </summary>
		public bool IsRelative
		{
			get { return _isRelative; }
			set { _isRelative = value; }
		}

		/// <summary>
		/// Name of this profile
		/// </summary>
		public string Name
		{
			get { return _name; }
			set { _name = value; }
		}

		/// <summary>
		/// The profile number ([ProfileX])
		/// </summary>
		public int Number
		{
			get { return _number; }
			set { _number = value; }
		}

		/// <summary>
		/// The user preferences for this profile
		/// </summary>
		public Preferences Preferences
		{
			get
			{
				if (_preferences == null)
				{
					_preferences = new Preferences(this);
				}
				return _preferences;
			}
		}

		#endregion

		#region Public Static Properties

		/// <summary>
		/// The folder that contains the profiles.ini file and the profiles folder
		/// </summary>
		private static string ProfilesPath
		{
			get { return string.Format("{0}{1}", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), PROFILES_PATH); }
		}

		/// <summary>
		/// The full path to the profiles.ini file
		/// </summary>
		private static string ProfilesIniFile
		{
			get { return ProfilesPath + PROFILES_FILENAME; }
		}

		/// <summary>
		/// The default profile (i.e. the first one)
		/// </summary>
		public static Profile Default
		{
			get
			{
				return Profiles[0];
			}
		}

		/// <summary>
		/// The list of profiles found in the profiles.ini file, keyed on profile name
		/// </summary>
		public static List<Profile> Profiles
		{
			get
			{
				if (_profiles == null)
				{
					LoadProfiles();
				}

				return _profiles;
			}
		}

		#endregion

		#region Private Static Methods

		/// <summary>
		/// Load the profiles out of the profiles.ini file in AppData
		/// </summary>
		private static void LoadProfiles()
		{
			_profiles = new List<Profile>();

			try
			{
				StreamReader sr = File.OpenText(ProfilesIniFile);
				string profiles = sr.ReadToEnd();
				sr.Close();

				MatchCollection mc = Regex.Matches(profiles, PROFILES_PATTERN, RegexOptions.ExplicitCapture |
																			   RegexOptions.Singleline |
																			   RegexOptions.IgnorePatternWhitespace |
																			   RegexOptions.IgnoreCase);
				if (mc.Count > 0)
				{
					foreach (Match m in mc)
					{
						Profile profile =
							new Profile(int.Parse(m.Groups["number"].Value), m.Groups["name"].Value, (m.Groups["isrelative"].Value == "1"),
										m.Groups["path"].Value);
						_profiles.Add(profile);
					}
				}
			}
			catch (Exception ex)
			{
				throw new Exception(string.Format("Unable to load profiles.ini file '{0}'", ProfilesIniFile), ex);
			}
		}

		#endregion
	}

	public class Preferences : DictionaryBase
	{
		#region Constants

		private const string PREFS_FILENAME = "prefs.js";
		private const string PREFS_LINE_PATTERN = "user_pref\\(\"(?<key>[^\"]+?)\",\\s\"?(?<value>[^\"]+?)\"?\\)";
		private const string PREFS_LINE_FORMAT = "user_pref(\"{0}\", {1});";

		#endregion

		#region Constructors

		public Preferences(Profile profile)
		{
			CurrentProfile = profile;
			LoadPreferences(true);
		}

		#endregion

		#region Private Fields

		private Profile _currentProfile;

		#endregion

		#region Private Properties

		private Profile CurrentProfile
		{
			get { return _currentProfile; }
			set { _currentProfile = value; }
		}

		#endregion

		#region Private Methods

		/// <summary>
		/// Ensure the preferences are loaded
		/// </summary>
		/// <param name="force">If true the preferences will be reloaded from the file</param>
		private void LoadPreferences(bool force)
		{
			if ((force || InnerHashtable.Count == 0) && CurrentProfile != null)
			{
				StreamReader sr = File.OpenText(CurrentProfile.QualifiedPath + PREFS_FILENAME);
				string contents = sr.ReadToEnd();
				sr.Close();

				MatchCollection matches = Regex.Matches(contents, PREFS_LINE_PATTERN);
				if (matches != null && matches.Count > 0)
				{
					foreach (Match m in matches)
					{
						if (!InnerHashtable.ContainsKey(m.Groups["key"].Value.ToLower()))
						{
							InnerHashtable.Add(m.Groups["key"].Value.ToLower(), m.Groups["value"].Value);
						}
					}
				}
			}
		}

		/// <summary>
		/// Update the preferences file for the given profile
		/// </summary>
		/// <param name="profile"></param>
		private void Save()
		{
			try
			{
				using (StreamWriter writer = new StreamWriter(CurrentProfile.QualifiedPath + PREFS_FILENAME, false))
				{
					foreach (DictionaryEntry preference in InnerHashtable)
					{
						string value = (preference.Value.GetType() == typeof (string)
						                	? string.Format("\"{0}\"", preference.Value)
						                	: preference.Value.ToString());

						writer.WriteLine(PREFS_LINE_FORMAT, preference.Key, value);
					}

					writer.Close();
				}
			}
			catch (Exception ex)
			{
				throw new Exception(string.Format("Unable to write to prefs.js"), ex);
			}
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Return the preferences value for the given key
		/// </summary>
		/// <param name="key">The key to look for</param>
		/// <param name="defaultValue">A default value to return if it isn't found</param>
		/// <returns></returns>
		public string GetValue(string key, string defaultValue)
		{
			LoadPreferences(false);

			if (InnerHashtable.ContainsKey(key))
			{
				return InnerHashtable[key].ToString();
			}
			else
			{
				return defaultValue;
			}
		}

		/// <summary>
		/// Return the preferences value for the given key
		/// </summary>
		/// <param name="key">The key to look for</param>
		/// <param name="defaultValue">A default value to return if it isn't found</param>
		/// <returns></returns>
		public int GetValue(string key, int defaultValue)
		{
			int retval;
			if (!int.TryParse(GetValue(key, defaultValue.ToString()), out retval))
			{
				retval = defaultValue;
			}

			return retval;
		}

		/// <summary>
		/// Return the preferences value for the given key
		/// </summary>
		/// <param name="key">The key to look for</param>
		/// <param name="defaultValue">A default value to return if it isn't found</param>
		/// <returns></returns>
		public bool GetValue(string key, bool defaultValue)
		{
			bool retavl;
			if (!bool.TryParse(GetValue(key, defaultValue.ToString()), out retavl))
			{
				retavl = defaultValue;
			}
			return retavl;
		}

		/// <summary>
		/// Apply the preference to the prefs.js file
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		public void SetValue(string key, object value)
		{
			LoadPreferences(true);

			if (InnerHashtable.ContainsKey(key))
			{
				InnerHashtable[key] = value;
			}
			else
			{
				InnerHashtable.Add(key, value);
			}

			Save();
		}

		/// <summary>
		/// Remove the key from the prefs.js file
		/// </summary>
		/// <param name="key"></param>
		public void DeleteValue(string key)
		{
			LoadPreferences(true);

			if (InnerHashtable.ContainsKey(key))
			{
				InnerHashtable.Remove(key);
				Save();
			}
		}

		#endregion
	}

	internal class InternalHelper
	{
		public static bool FirefoxRunning
		{
			get
			{
				Process[] processes = Process.GetProcessesByName(Settings.Default.ProcessName);
				return (processes.Length > 0);
			}
		}
	}
}